/*
 * Copyright 2015 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.inferred.freebuilder.processor.property;

import static org.inferred.freebuilder.processor.GeneratedTypeSubject.assertThat;
import static org.inferred.freebuilder.processor.model.ClassTypeImpl.newTopLevelClass;
import static org.inferred.freebuilder.processor.source.FunctionalType.primitiveUnaryOperator;

import com.google.common.collect.ImmutableMap;

import org.inferred.freebuilder.processor.BuilderFactory;
import org.inferred.freebuilder.processor.Datatype;
import org.inferred.freebuilder.processor.GeneratedBuilder;
import org.inferred.freebuilder.processor.model.ClassTypeImpl;
import org.inferred.freebuilder.processor.model.PrimitiveTypeImpl;
import org.inferred.freebuilder.processor.property.PrimitiveOptionalProperty.OptionalType;
import org.inferred.freebuilder.processor.source.QualifiedName;
import org.inferred.freebuilder.processor.source.feature.GuavaLibrary;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.util.Optional;

@RunWith(JUnit4.class)
public class PrimitiveOptionalSourceTest {

  @Test
  public void testSource() {
    assertThat(builder()).given(GuavaLibrary.AVAILABLE).generates(
        "// Autogenerated code. Do not modify.",
        "package com.example;",
        "",
        "import com.example.Item;",
        "import com.google.common.annotations.VisibleForTesting;",
        "import java.util.Objects;",
        "import java.util.OptionalDouble;",
        "import java.util.OptionalInt;",
        "import java.util.function.DoubleUnaryOperator;",
        "import java.util.function.IntUnaryOperator;",
        "",
        "/** Auto-generated superclass of {@link Item.Builder}, "
            + "derived from the API of {@link Item}. */",
        "abstract class Item_Builder {",
        "",
        "  /**",
        "   * Creates a new builder using {@code value} as a template.",
        "   *",
        "   * <p>If {@code value} is a partial, the builder will return more partials.",
        "   */",
        "  public static Item.Builder from(Item value) {",
        "    if (value instanceof Rebuildable) {",
        "      return ((Rebuildable) value).toBuilder();",
        "    } else {",
        "      return new Item.Builder().mergeFrom(value);",
        "    }",
        "  }",
        "",
        "  private OptionalInt cost = OptionalInt.empty();",
        "  private OptionalDouble tax = OptionalDouble.empty();",
        "",
        "  /**",
        "   * Sets the value to be returned by {@link Item#cost()}.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Item.Builder cost(int cost) {",
        "    this.cost = OptionalInt.of(cost);",
        "    return (Item.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Sets the value to be returned by {@link Item#cost()}.",
        "   *",
        "   * @return this {@code Builder} object",
        "   * @throws NullPointerException if {@code cost} is null",
        "   */",
        "  public Item.Builder cost(OptionalInt cost) {",
        "    if (cost.isPresent()) {",
        "      return cost(cost.getAsInt());",
        "    } else {",
        "      return clearCost();",
        "    }",
        "  }",
        "",
        "  /**",
        "   * If the value to be returned by {@link Item#cost()} is present, "
            + "replaces it by applying {@code",
        "   * mapper} to it and using the result.",
        "   *",
        "   * @return this {@code Builder} object",
        "   * @throws NullPointerException if {@code mapper} is null",
        "   */",
        "  public Item.Builder mapCost(IntUnaryOperator mapper) {",
        "    Objects.requireNonNull(mapper);",
        "    cost.ifPresent(value -> cost(mapper.applyAsInt(value)));",
        "    return (Item.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Sets the value to be returned by {@link Item#cost()} to "
            + "{@link OptionalInt#empty()}.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Item.Builder clearCost() {",
        "    cost = OptionalInt.empty();",
        "    return (Item.Builder) this;",
        "  }",
        "",
        "  /** Returns the value that will be returned by {@link Item#cost()}. */",
        "  public OptionalInt cost() {",
        "    return cost;",
        "  }",
        "",
        "  /**",
        "   * Sets the value to be returned by {@link Item#tax()}.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Item.Builder tax(double tax) {",
        "    this.tax = OptionalDouble.of(tax);",
        "    return (Item.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Sets the value to be returned by {@link Item#tax()}.",
        "   *",
        "   * @return this {@code Builder} object",
        "   * @throws NullPointerException if {@code tax} is null",
        "   */",
        "  public Item.Builder tax(OptionalDouble tax) {",
        "    if (tax.isPresent()) {",
        "      return tax(tax.getAsDouble());",
        "    } else {",
        "      return clearTax();",
        "    }",
        "  }",
        "",
        "  /**",
        "   * If the value to be returned by {@link Item#tax()} is present, "
            + "replaces it by applying {@code",
        "   * mapper} to it and using the result.",
        "   *",
        "   * @return this {@code Builder} object",
        "   * @throws NullPointerException if {@code mapper} is null",
        "   */",
        "  public Item.Builder mapTax(DoubleUnaryOperator mapper) {",
        "    Objects.requireNonNull(mapper);",
        "    tax.ifPresent(value -> tax(mapper.applyAsDouble(value)));",
        "    return (Item.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Sets the value to be returned by {@link Item#tax()} to "
            + "{@link OptionalDouble#empty()}.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Item.Builder clearTax() {",
        "    tax = OptionalDouble.empty();",
        "    return (Item.Builder) this;",
        "  }",
        "",
        "  /** Returns the value that will be returned by {@link Item#tax()}. */",
        "  public OptionalDouble tax() {",
        "    return tax;",
        "  }",
        "",
        "  /**",
        "   * Copies values from {@code value}, skipping empty optionals.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Item.Builder mergeFrom(Item value) {",
        "    value.cost().ifPresent(this::cost);",
        "    value.tax().ifPresent(this::tax);",
        "    return (Item.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Copies values from {@code template}, skipping empty optionals.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Item.Builder mergeFrom(Item.Builder template) {",
        "    template.cost().ifPresent(this::cost);",
        "    template.tax().ifPresent(this::tax);",
        "    return (Item.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Resets the state of this builder.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Item.Builder clear() {",
        "    Item_Builder defaults = new Item.Builder();",
        "    cost = defaults.cost;",
        "    tax = defaults.tax;",
        "    return (Item.Builder) this;",
        "  }",
        "",
        "  /** Returns a newly-created {@link Item} based on the contents of this "
            + "{@code Builder}. */",
        "  public Item build() {",
        "    return new Value(this);",
        "  }",
        "",
        "  /**",
        "   * Returns a newly-created partial {@link Item} for use in unit tests. "
            + "State checking will not be",
        "   * performed.",
        "   *",
        "   * <p>The builder returned by {@link Item.Builder#from(Item)} will propagate the "
            + "partial status of",
        "   * its input, overriding {@link Item.Builder#build() build()} to return another "
            + "partial. This",
        "   * allows for robust tests of modify-rebuild code.",
        "   *",
        "   * <p>Partials should only ever be used in tests. "
            + "They permit writing robust test cases that won't",
        "   * fail if this type gains more application-level constraints "
            + "(e.g. new required fields) in",
        "   * future. If you require partially complete values in production code, "
            + "consider using a Builder.",
        "   */",
        "  @VisibleForTesting()",
        "  public Item buildPartial() {",
        "    return new Partial(this);",
        "  }",
        "",
        "  private abstract static class Rebuildable extends Item {",
        "    public abstract Item.Builder toBuilder();",
        "  }",
        "",
        "  private static final class Value extends Rebuildable {",
        "    private final OptionalInt cost;",
        "    private final OptionalDouble tax;",
        "",
        "    private Value(Item_Builder builder) {",
        "      this.cost = builder.cost;",
        "      this.tax = builder.tax;",
        "    }",
        "",
        "    @Override",
        "    public OptionalInt cost() {",
        "      return cost;",
        "    }",
        "",
        "    @Override",
        "    public OptionalDouble tax() {",
        "      return tax;",
        "    }",
        "",
        "    @Override",
        "    public Item.Builder toBuilder() {",
        "      Item_Builder builder = new Item.Builder();",
        "      builder.cost = cost;",
        "      builder.tax = tax;",
        "      return (Item.Builder) builder;",
        "    }",
        "",
        "    @Override",
        "    public boolean equals(Object obj) {",
        "      if (!(obj instanceof Value)) {",
        "        return false;",
        "      }",
        "      Value other = (Value) obj;",
        "      return Objects.equals(cost, other.cost) && Objects.equals(tax, other.tax);",
        "    }",
        "",
        "    @Override",
        "    public int hashCode() {",
        "      return Objects.hash(cost, tax);",
        "    }",
        "",
        "    @Override",
        "    public String toString() {",
        "      StringBuilder result = new StringBuilder(\"Item{\");",
        "      String separator = \"\";",
        "      if (cost.isPresent()) {",
        "        result.append(\"cost=\").append(cost.getAsInt());",
        "        separator = \", \";",
        "      }",
        "      if (tax.isPresent()) {",
        "        result.append(separator).append(\"tax=\").append(tax.getAsDouble());",
        "      }",
        "      return result.append(\"}\").toString();",
        "    }",
        "  }",
        "",
        "  private static final class Partial extends Rebuildable {",
        "    private final OptionalInt cost;",
        "    private final OptionalDouble tax;",
        "",
        "    Partial(Item_Builder builder) {",
        "      this.cost = builder.cost;",
        "      this.tax = builder.tax;",
        "    }",
        "",
        "    @Override",
        "    public OptionalInt cost() {",
        "      return cost;",
        "    }",
        "",
        "    @Override",
        "    public OptionalDouble tax() {",
        "      return tax;",
        "    }",
        "",
        "    private static class PartialBuilder extends Item.Builder {",
        "      @Override",
        "      public Item build() {",
        "        return buildPartial();",
        "      }",
        "    }",
        "",
        "    @Override",
        "    public Item.Builder toBuilder() {",
        "      Item_Builder builder = new PartialBuilder();",
        "      builder.cost = cost;",
        "      builder.tax = tax;",
        "      return (Item.Builder) builder;",
        "    }",
        "",
        "    @Override",
        "    public boolean equals(Object obj) {",
        "      if (!(obj instanceof Partial)) {",
        "        return false;",
        "      }",
        "      Partial other = (Partial) obj;",
        "      return Objects.equals(cost, other.cost) && Objects.equals(tax, other.tax);",
        "    }",
        "",
        "    @Override",
        "    public int hashCode() {",
        "      return Objects.hash(cost, tax);",
        "    }",
        "",
        "    @Override",
        "    public String toString() {",
        "      StringBuilder result = new StringBuilder(\"partial Item{\");",
        "      String separator = \"\";",
        "      if (cost.isPresent()) {",
        "        result.append(\"cost=\").append(cost.getAsInt());",
        "        separator = \", \";",
        "      }",
        "      if (tax.isPresent()) {",
        "        result.append(separator).append(\"tax=\").append(tax.getAsDouble());",
        "      }",
        "      return result.append(\"}\").toString();",
        "    }",
        "  }",
        "}");
  }

  private static GeneratedBuilder builder() {
    ClassTypeImpl optionalInt = newTopLevelClass("java.util.OptionalInt");
    ClassTypeImpl optionalDouble = newTopLevelClass("java.util.OptionalDouble");
    QualifiedName person = QualifiedName.of("com.example", "Item");
    QualifiedName generatedBuilder = QualifiedName.of("com.example", "Item_Builder");

    Datatype datatype = new Datatype.Builder()
        .setBuilder(person.nestedType("Builder").withParameters())
        .setExtensible(true)
        .setBuilderFactory(BuilderFactory.NO_ARGS_CONSTRUCTOR)
        .setBuilderSerializable(false)
        .setGeneratedBuilder(generatedBuilder.withParameters())
        .setInterfaceType(false)
        .setPartialType(generatedBuilder.nestedType("Partial").withParameters())
        .setPropertyEnum(generatedBuilder.nestedType("Property").withParameters())
        .setRebuildableType(generatedBuilder.nestedType("Rebuildable").withParameters())
        .setType(person.withParameters())
        .setValueType(generatedBuilder.nestedType("Value").withParameters())
        .build();
    Property cost = new Property.Builder()
        .setAllCapsName("COST")
        .setCapitalizedName("Cost")
        .setFullyCheckedCast(true)
        .setGetterName("cost")
        .setName("cost")
        .setType(optionalInt)
        .setUsingBeanConvention(false)
        .build();
    Property tax = new Property.Builder()
        .setAllCapsName("TAX")
        .setCapitalizedName("Tax")
        .setFullyCheckedCast(true)
        .setGetterName("tax")
        .setName("tax")
        .setType(optionalDouble)
        .setUsingBeanConvention(false)
        .build();

    return new GeneratedBuilder(datatype, ImmutableMap.of(
        cost, new PrimitiveOptionalProperty(
            datatype,
            cost,
            OptionalType.INT,
            Optional.of(primitiveUnaryOperator(PrimitiveTypeImpl.INT)),
            Optional.empty()),
        tax, new PrimitiveOptionalProperty(
            datatype,
            tax,
            OptionalType.DOUBLE,
            Optional.of(primitiveUnaryOperator(PrimitiveTypeImpl.DOUBLE)),
            Optional.empty())));
  }
}
